1. Импорт

In [1]:
#!pip install tapi-yandex-metrika
#!pip install wordcloud
In [2]:
import pandas as pd
import numpy as np
import scipy
from pandas.io.json import json_normalize
import json
from tapi_yandex_metrika import YandexMetrikaStats
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import smtplib
from email.mime.base import MIMEBase
from email import encoders
from datetime import date
import datetime
import os
from datetime import timedelta
import math
from win32com.client import Dispatch
import logging
import traceback
import sys
###
import matplotlib.pyplot as plt
import plotly.express as px
import seaborn as sns
from wordcloud import WordCloud, STOPWORDS
from IPython.core.display import display, HTML
import plotly.graph_objects as go
from plotly.subplots import make_subplots

2. Сбор и анализ данных

Указываем счетчик из открытого источника данных - Yandex.Metrica Demo.
Номер счетчика - 44147844

In [3]:
ACCESS_TOKEN = # Здесь нужно указать свой токен
METRIC_IDS = "44147844"

api = YandexMetrikaStats(
    access_token=ACCESS_TOKEN)

Указываем дату. Разницу timedelta = 30 days я взял вместо явного указания одного месяца, так как в январе-феврале и феврале-марте результирующая разница в данных может сильно отличаться из-за 3 недостающих дней

In [4]:
# укажем диапазон через datetime
End_Second_Range_Date = date.today().replace(day=1) - timedelta(days=1)
Start_Second_Range_Date = End_Second_Range_Date - timedelta(days=30)
End_First_Range_Date = Start_Second_Range_Date - timedelta(days=1)
Start_First_Range_Date = End_First_Range_Date - timedelta(days=30)
# Переведем в string и поменяем / на -
Start_Second_Range_Date = Start_Second_Range_Date.strftime('%Y/%m/%d').replace("/","-")
End_Second_Range_Date = End_Second_Range_Date.strftime('%Y/%m/%d').replace("/","-")
Start_First_Range_Date = Start_First_Range_Date.strftime('%Y/%m/%d').replace("/","-")
End_First_Range_Date = End_First_Range_Date.strftime('%Y/%m/%d').replace("/","-")
In [5]:
display(HTML(f"<div style='text-align:center'>{'Начало и конец 1-го временного интервала:'}</div>"),
        HTML(f"<div style='text-align:center'>{Start_First_Range_Date}</div><div style='text-align:center'>{End_First_Range_Date}</div>"))
display(HTML(f"<div style='text-align:center'>{'Начало и конец 2-го временного интервала:'}</div>"),
        HTML(f"<div style='text-align:center'>{Start_Second_Range_Date}<div style='text-align:center'>{End_Second_Range_Date}</div>"))
Начало и конец 1-го временного интервала:
2022-10-31
2022-11-30
Начало и конец 2-го временного интервала:
2022-12-01
2022-12-31

Проверим подключение к публичным данным на примере выполнения простой задачи:

2.1 Отследить динамику изменения популярности разных операционных систем на сайте за 2 разных временных промежутка.

Для дальнейшего обращения к Яндекс.Метрике через API предварительно нужно записать параметры в переменную. По этим параметрам запроса Яндекс.Метрика отдаст необходимый нам срез данных. Это может быть, например, количество сессий за прошлую неделю на главной странице или количество самых разных поисковых запросов перед посещением искомого сайта.
Создадим функцию request, аргументами которой будут выступать интересующие нас временные интервалы и параметры самого запроса.

In [6]:
def request(Start_First_Date, End_First_Date, Start_Second_Date, End_Second_Date, metrics_params, dimensions_params, filters_params):
    # Укажем параметры для первого временного окна
    range_params_first = dict(
        ids=METRIC_IDS,
        metrics = metrics_params,
        dimensions = dimensions_params,
        filters = filters_params,
        date1 = Start_First_Date,
        date2 = End_First_Date,
        accuracy="full",
        limit = 100000, 
        offset=1, 
        pretty=True
    )
    result_first = api.stats().get(params=range_params_first) # Записываем в переменную result_first данные, полученные за первый временной интервал
    # Укажем параметры для второго временного окна
    range_params_second = dict(
        ids=METRIC_IDS,
        metrics = metrics_params,
        dimensions = dimensions_params,
        filters = filters_params,
        date1 = Start_Second_Date,
        date2 = End_Second_Date,
        accuracy="full",
        limit = 100000,
        offset=1,
        pretty=True
    )
    result_second = api.stats().get(params=range_params_second) # Записываем в переменную result_second данные, полученные за второй временной интервал 
    return result_first(), result_second()

Аргументы функции request:

  1. Начало первого временного окна
  2. Конец первого временного окна
  3. Начало второго временного окна
  4. Конец второго временного окна
  5. Применяемые метрики к запросу
  6. Применяемые группировки к запросу
  7. Фильтры в запросе

По итогу выполнения такой функции мы совершаем два запроса с разными временными промежутками, но одинаковыми параметрами. Таким образом меняя параметры, мы можем записывать два результата в переменные и сравнивать эти показатели. В нашем случае нам нужно разделение по операционным системам - ym:s:operatingSystem, а метрикой будет выступать количество сессий - ym:s:visits.

In [7]:
metrics = "ym:s:visits"
dimensions = 'ym:s:operatingSystem'
filters = ""

first_request, second_request = request('2022-10-31', '2022-11-30', '2022-12-01', '2022-12-31',"ym:s:visits", 'ym:s:operatingSystem', '')
# ИЛИ так как мы ранее обьявляли переменные с временем, то можно предыдущую строку перезаписать, как:
first_request, second_request = request(Start_First_Range_Date, End_First_Range_Date, Start_Second_Range_Date, End_Second_Range_Date,metrics, dimensions, filters)

Но пока эти результаты в очень сыром виде, их нужно обработать, напишем для этого функцию processing, которая будет обрабатывать наши requests и на выходе отдавать обработанные данные:

In [8]:
def processing(group_name, first_request, second_request):
    # 1. Приведем полученные данные в датафреймы 
    data_first = pd.json_normalize(first_request.data['data']) 
    data_second = pd.json_normalize(second_request.data['data']) 

    # 2. Уберем квадратные скобки у данных, а также раскроем JSON формат данных разбив вложенные данные на несколько столбцов 
    clear_data_first = pd.concat([data_first.explode(["dimensions","metrics"])["metrics"].
                                  reset_index(drop=True), pd.json_normalize(data_first.explode(["dimensions","metrics"])
                                                                            ["dimensions"])], axis=1).rename({'metrics':'first_metric'}, axis = 1)
    clear_data_second = pd.concat([data_second.explode(["dimensions","metrics"])["metrics"].
                                  reset_index(drop=True), pd.json_normalize(data_second.explode(["dimensions","metrics"])
                                                                            ["dimensions"])], axis=1).rename({'metrics':'second_metric'}, axis = 1)

    # 3. Обьединим два датасета в один набор и удалим ненужные столбцы
    df_helper = pd.merge(clear_data_first,clear_data_second,how='outer', left_on = 'name', right_on = 'name')\
    .rename({'first_metric':'Метрика за первый временной интервал','name':group_name,
                                                         'second_metric':'Метрика за второй временной интервал'},axis=1)

    # 4. Поменяем порядок столбцов для более легкого визуального восприятия, а также удалим строки с пустыми данными 
    df_task = df_helper.reindex(columns=[group_name,'Метрика за первый временной интервал','Метрика за второй временной интервал',])
    
    # 5. Удалим пустые и 0 значения.
    df_task= df_task.loc[(df_task['Метрика за первый временной интервал'] != 0) & (df_task['Метрика за первый временной интервал'] != 0)].dropna()

    # 6. Добавим столбец с количественной разницей в сессиях 
    df_task['Количественная разница'] = df_task['Метрика за второй временной интервал'] - df_task['Метрика за первый временной интервал']

    # 7. Добавим столбец с процентной разницей
    list_columns = df_task.columns.tolist()[1:] # здесь 1:, потому что берем все кроме 1-го столбца, он в string
    try:                        # Если метрика не целочисленная, то возникнет ошибка, поэтому добавим конструкцию с try-except
        for i in list_columns:
            df_task[i] = df_task[i].astype('Int32')
    except TypeError:
        for i in list_columns:
            df_task[i] = df_task[i].astype('Float64')
    df_task['Процентная разница'] = (round(((df_task['Метрика за второй временной интервал'] / df_task['Метрика за первый временной интервал'])-1)*100,2))
    df_task.head(10)
    return(df_task)

Аргументы функции processing:

  1. Свое кастомное название для группировки
  2. Первый запрос
  3. Второй запрос

Выполним функцию processing и запишем в переменную df_first_task

In [9]:
df_first_task = processing('Операционная система', first_request, second_request)
df_first_task.head(5)
Out[9]:
Операционная система Метрика за первый временной интервал Метрика за второй временной интервал Количественная разница Процентная разница
0 Windows 10 (и последующие) 13032 10283 -2749 -21.09
1 macOS Catalina (и последующие) 3072 2765 -307 -9.99
2 Windows 7 or 2008 Server 1342 1014 -328 -24.44
3 Android 11 931 1374 443 47.58
4 Android 12 700 1084 384 54.86

Из таблицы выше мы наблюдаем 5 самых популярных операционных систем:

  1. Windows 10 (и последующие)
  2. macOS Catalina (и последующие)
  3. Android 11
  4. Android 12
  5. Windows 7 or 2008 Server
In [10]:
# Сформируем на основе вышеприведенной таблицы две других
display(HTML(f"<div style='text-align:center'>{'Таблица с операционными системами, у которых наблюдался самый большой процентный прирост по количеству сессий:'}</div>"))
display(df_first_task.sort_values(by='Процентная разница', ascending=False).head(5).reset_index().drop('index', axis=1))
display(HTML(f"<div style='text-align:center'>{'И аналогичная таблица с операционными системами, у которых наблюдался самый большой процентный спад по количеству сессий:'}</div>"))
display(df_first_task.sort_values(by='Процентная разница').head(5).reset_index().drop('index', axis=1))
Таблица с операционными системами, у которых наблюдался самый большой процентный прирост по количеству сессий:
Операционная система Метрика за первый временной интервал Метрика за второй временной интервал Количественная разница Процентная разница
0 Android 13 34 113 79 232.35
1 Windows XP 5 16 11 220.0
2 iOS 11 7 15 8 114.29
3 Google Android 7.1 Nougat 29 53 24 82.76
4 Google Android 8.1 Oreo 113 206 93 82.3
И аналогичная таблица с операционными системами, у которых наблюдался самый большой процентный спад по количеству сессий:
Операционная система Метрика за первый временной интервал Метрика за второй временной интервал Количественная разница Процентная разница
0 OS X Yosemite 12 1 -11 -91.67
1 Windows Mobile (другие или не определено) 8 1 -7 -87.5
2 Mac OS (другие) 15 5 -10 -66.67
3 OS X Mavericks 2 1 -1 -50.0
4 Google Android 5.0 Lollipop 8 4 -4 -50.0

Как видно из полученной таблицы выше, самый большой положительный прирост по сессиям, выраженный в процентном соотношении, был у таких операционных систем, как:

  1. Android 13
  2. Windows XP
  3. iOS 11
  4. Google Android 7.1 Nougat
  5. Google Android 8.1 Oreo
    Самый большой процентный спад был у:
  6. OS X Yosemite
  7. Windows Mobile (другие или не определено)
  8. Mac OS (другие)
  9. Google Android 5.0 Lollipop
  10. OS X Mavericks

Построим столбчатые диаграммы, в которых проследим разницу популярности разных операционных систем за два разных временных промежутка:

In [11]:
# Дублируем строки и в продублированных выставляем флаг 'True'
first_task_barplot = pd.concat([df_first_task] * 2).sort_index().reset_index(drop=True)
first_task_barplot['Временной интервал'] = first_task_barplot.duplicated().astype(str)
# Заменяем 'False' на 'Первый временной интервал', а True' на 'Второй временной интервал'
first_task_barplot['Временной интервал'] = first_task_barplot.replace('False','Первый временной интервал').\
    replace('True','Второй временной интервал')['Временной интервал']
# Склеиваем два датафрейма, одновременно с этим переименовывая столбцы
first_task_barplot = pd.concat([first_task_barplot.loc[first_task_barplot['Временной интервал'] == 'Первый временной интервал']
                                [['Операционная система','Метрика за первый временной интервал', 'Временной интервал']]
                                .rename({'Метрика за первый временной интервал':'Метрика'}, axis = 1), 
                                first_task_barplot.loc[first_task_barplot['Временной интервал'] == 'Второй временной интервал']
                                [['Операционная система','Метрика за второй временной интервал', 'Временной интервал']]
                                .rename({'Метрика за второй временной интервал':'Метрика'}, axis = 1)]) 
# Строим столбчатые диаграммы
fig = px.bar(first_task_barplot, 
             x="Временной интервал",
             y="Метрика", 
             color="Операционная система", 
             text="Операционная система", 
             width=1000, height=670, opacity = 0.75)
fig.show()

2.2 Перейдем к чему-то посложнее: посчитаем количество сессий, начатых со всех страниц входа, кроме: https://metrica.yandex.com/about

Это не решить уже «в лоб» без применения фильтрации (filters). Фильтрацию организуем через регулярное выражение и запись вида =~'regular_expression'

In [12]:
metrics = "ym:s:visits"
dimensions = 'ym:s:startURL'
filters = "EXISTS(ym:s:startURL=~'^https://metrica.yandex.com/about/..*')" # Этим регулярным выражением мы откидываем одну 
# страницу, но можно написать и по другому, составив список исключений через NONE

first_request, second_request = request(Start_First_Range_Date, End_First_Range_Date, Start_Second_Range_Date, End_Second_Range_Date, metrics, dimensions, filters)
In [13]:
df_second_task = processing('URL сайта', first_request, second_request)
df_second_task.head(10)
Out[13]:
URL сайта Метрика за первый временной интервал Метрика за второй временной интервал Количественная разница Процентная разница
0 https://metrica.yandex.com/about/info/privacy-... 1376 2757 1381 100.36
1 https://metrica.yandex.com/about/info/integrat... 387 109 -278 -71.83
2 https://metrica.yandex.com/about/info/pricing 310 247 -63 -20.32
3 https://metrica.yandex.com/about/info/traffic 102 101 -1 -0.98
4 https://metrica.yandex.com/about/info/features 94 81 -13 -13.83
5 https://metrica.yandex.com/about/info/data-pol... 82 81 -1 -1.22
6 https://metrica.yandex.com/about/info/behavior 67 62 -5 -7.46
7 https://metrica.yandex.com/about/info/audience 64 46 -18 -28.12
8 https://metrica.yandex.com/about/info/gdpr 44 46 2 4.55
9 https://metrica.yandex.com/about/info/performance 43 51 8 18.6

2.3 Усложним еще больше задачу: Составим список самых популярных поисковых запросов, распределив их по 3 группам

Мы разделим на 3 группы - низкочастотные, среднечастотные, высокочастотные запросы. Они различаются между собой по частоте обращения к поисковым системам за выбранный промежуток времени.

  1. Низкочастотные запросы (НЧ) - это наименее запрашиваемые поисковые запросы за выбранный промежуток времени. Как правило, это редкий и уникальный запрос. Например, яндекс метрика вход в личный кабинет мои компании.

  2. Среднечастотный трафик (СЧ) - это запросы в поисковой системе, по которым зафиксировано среднее количество обращений от пользователей за выбранный промежуток времени. Выглядят как НЧ запросы, но имеют более общую формулировку, например, вход в яндекс метрику.

  3. Высокочастотный трафик (ВЧ) - это наиболее запрашиваемые поисковые запросы за выбранный промежуток времени. Такой запрос обычно формулируется в общем, например, яндекс метрика.

Верхняя и нижняя границы этих групп очень условные и зависят от множества факторов. Я буду отталкиваться от такого распределения:

  1. НЧ - до 50 показов
  2. СЧ - от 50 и до 500 показов
  3. ВЧ - от 500 показов Формируем таблицу со всеми возможными запросами:
In [14]:
metrics = "ym:s:visits"
dimensions = 'ym:s:lastSearchPhrase'
filters = "(EXISTS(ym:s:<attribution>SearchPhrase=*'*яндекс*')\
    OR EXISTS(ym:s:<attribution>SearchPhrase=*'*метрика*')\
    OR EXISTS(ym:s:<attribution>SearchPhrase=*'*yandex*')\
    OR EXISTS(ym:s:<attribution>SearchPhrase=*'*metrika*'))"

first_request, second_request = request(Start_First_Range_Date, End_First_Range_Date, Start_Second_Range_Date, End_Second_Range_Date, metrics, dimensions, filters)

Можно переписать параметр filters из ячейки выше нижеизложенным вариантом, как раз отрабатывая фильтрацию вместе с параметром NONE.
В таком случае мы избавимся от поисковых запросов, включающих в себя слова Яндекс и Yandex

In [15]:
filters="(EXISTS(ym:s:<attribution>SearchPhrase=*'*яндекс*')\
        OR EXISTS(ym:s:<attribution>SearchPhrase=*'*метрика*')\
        OR EXISTS(ym:s:<attribution>SearchPhrase=*'*yandex*')\
        OR EXISTS(ym:s:<attribution>SearchPhrase=*'*metrika*'))\
        AND \
        (NONE(ym:s:<attribution>SearchPhrase=*'*яндекс*') AND NONE(ym:s:<attribution>SearchPhrase=*'*yandex*'))"
In [16]:
df_third_task = processing('Поисковый запрос', first_request, second_request)
df_third_task
Out[16]:
Поисковый запрос Метрика за первый временной интервал Метрика за второй временной интервал Количественная разница Процентная разница
0 яндекс метрика 4633 3288 -1345 -29.03
1 метрика 2097 1649 -448 -21.36
2 метрика яндекс 1194 1115 -79 -6.62
3 yandex metrika 188 161 -27 -14.36
4 metrika.yandex.ru 96 111 15 15.62
... ... ... ... ... ...
257 яндекс метрики вход в личный кабинет 1 1 0 0.0
264 яндекс статистика сайта метрика 1 1 0 0.0
267 яндекс счётчик 1 1 0 0.0
268 яндекс. метрика 1 4 3 300.0
277 янжекс метрика 1 1 0 0.0

125 rows × 5 columns

Разделим полученную таблицу выше на три группы по описанным выше критериям:

In [17]:
display(HTML(f"<div style='text-align:center'>{'Таблица с НЧ запросами'}</div>"))
display(df_third_task.loc[(df_third_task['Метрика за первый временной интервал'] < 50) | (df_third_task['Метрика за второй временной интервал'] < 50)])
display(HTML(f"<div style='text-align:center'>{'Таблица с СЧ запросами'}</div>"))
display(df_third_task.loc[((df_third_task['Метрика за первый временной интервал'] > 50) &
                  (df_third_task['Метрика за первый временной интервал'] < 500)) |
                 ((df_third_task['Метрика за второй временной интервал'] > 50) &
                  (df_third_task['Метрика за второй временной интервал'] < 500))])
display(HTML(f"<div style='text-align:center'>{'Таблица с ВЧ запросами'}</div>"))
display(df_third_task.loc[(df_third_task['Метрика за первый временной интервал'] > 500) |
                          (df_third_task['Метрика за второй временной интервал'] > 500)])
Таблица с НЧ запросами
Поисковый запрос Метрика за первый временной интервал Метрика за второй временной интервал Количественная разница Процентная разница
11 metrika 46 37 -9 -19.57
12 metrika yandex 46 50 4 8.7
13 яндекс метрика вход в личный кабинет 32 40 8 25.0
14 yandex.metrika 24 20 -4 -16.67
15 метрика яндекс метрика 21 6 -15 -71.43
... ... ... ... ... ...
257 яндекс метрики вход в личный кабинет 1 1 0 0.0
264 яндекс статистика сайта метрика 1 1 0 0.0
267 яндекс счётчик 1 1 0 0.0
268 яндекс. метрика 1 4 3 300.0
277 янжекс метрика 1 1 0 0.0

114 rows × 5 columns

Таблица с СЧ запросами
Поисковый запрос Метрика за первый временной интервал Метрика за второй временной интервал Количественная разница Процентная разница
3 yandex metrika 188 161 -27 -14.36
4 metrika.yandex.ru 96 111 15 15.62
5 яндекс.метрика 87 55 -32 -36.78
6 metrika yandex ru 86 80 -6 -6.98
7 яндекс метрики 79 68 -11 -13.92
8 yandex метрика 66 50 -16 -24.24
9 metrica.yandex.com 57 59 2 3.51
10 я метрика 52 58 6 11.54
Таблица с ВЧ запросами
Поисковый запрос Метрика за первый временной интервал Метрика за второй временной интервал Количественная разница Процентная разница
0 яндекс метрика 4633 3288 -1345 -29.03
1 метрика 2097 1649 -448 -21.36
2 метрика яндекс 1194 1115 -79 -6.62
In [18]:
df_third_task['Поисковый запрос'].str.split().explode().value_counts().to_frame().rename({'Поисковый запрос':'Ключевые слова'},axis=1).head(3)
Out[18]:
Ключевые слова
яндекс 65
метрика 53
yandex 17

Как видно из таблицы выше, три самых часто встречаемых ключевых слов в запросах - это:

  1. яндекс
  2. метрика
  3. yandex

Построим облако слов:

In [19]:
# Сделаем разметку графиков в центре 
HTML("""
<style>
.output_png {
    display: table-cell;
    text-align: center;
    vertical-align: middle;
}
</style>
""")
Out[19]:
In [20]:
# Меняем цвет облака в двухцветный формат
def double_color_func(word, font_size, position,orientation,random_state=None, **kwargs):
    return("hsl(0,100%, 1%)")
display(HTML(f"<div style='text-align:center'>{'Количество уникальных слов для облака:'}</div>"), HTML(f"<div style='text-align:center'>{len(set(df_third_task['Поисковый запрос'].str.cat(sep=' ').split(' ')))}</div>"))
# Сменим цвет фона на белый, максимальное количество слов на 85, высоту и ширину к 3000 и 2000 соответственно 
wordcloud = WordCloud( background_color="gold", width=2000, height=2000, max_words=85).generate(df_third_task['Поисковый запрос'].str.cat(sep=' '))
# Сделаем цвет букв черным
wordcloud.recolor(color_func = double_color_func)
# Установим размер облака
plt.figure(figsize=[15,10])
# Строим облако
plt.imshow(wordcloud, interpolation="bilinear")
# Убираем координатные оси
plt.axis("off")
# Убираем комментарии
plt.show()
Количество уникальных слов для облака:
85

2.4 Сделать визуализацию глубины просмотра по разным страницам сайта¶

In [21]:
# Обьявим временный пустой датафрейм, в который будем помещать данные с двумя метриками последовательно
df_temp = pd.DataFrame()

# Через цикл вызываем ф-ию два раза и затем склеиваем два полученных датафрейма 
for metrics in ("ym:s:pageviews",'ym:s:visits'):
    dimensions = 'ym:s:startURL'
    filters = "EXISTS(ym:s:startURL=~'^https://metrica.yandex.com/about*')"
    
    first_request, second_request = request(Start_First_Range_Date, End_First_Range_Date, Start_Second_Range_Date, End_Second_Range_Date,metrics, dimensions, filters)
    df_temp = pd.concat([df_temp,processing(metrics, first_request, second_request)], axis=1)
# Обьединим датафрейм с самим собой по столбцам сессий и показа страниц     
df_fourth_task = pd.merge(df_temp, df_temp, how='outer', left_on = 'ym:s:pageviews', right_on = 'ym:s:visits')
In [22]:
# Нас интересует в полученном датафрейме только часть столбцов, поэтому уберем ненужные, заодно переименовав столбцы в приемлимый вариант
df_fourth_task = pd.concat([df_fourth_task.iloc[:,0:5],df_fourth_task.iloc[:,15:20]],axis=1).dropna().rename({'ym:s:pageviews_x':'URL',
                                                                          'Метрика за первый временной интервал_x':'Суммарная глубина просмотра за первый временной интервал',
                                                                          'Метрика за второй временной интервал_x':'Суммарная глубина просмотра за второй временной интервал',
                                                                          'Количественная разница_x':'Количественная разница суммарной глубины просмотра',
                                                                          'Процентная разница_x':'Процентная разница суммарной глубины просмотра',
                                                                          'Метрика за первый временной интервал_y':'Число сессий за первый временной интервал',
                                                                          'Метрика за второй временной интервал_y':'Число сессий за второй временной интервал',
                                                                          'Количественная разница_y':'Количественная разница числа сессий',
                                                                          'Процентная разница_y':'Процентная разница числа сессий'}, axis=1).drop('ym:s:visits_y',axis=1)
# Поменяем порядок столбцов в датафрейме 
df_fourth_task = df_fourth_task.reindex(columns=['URL','Суммарная глубина просмотра за первый временной интервал',
                         'Суммарная глубина просмотра за второй временной интервал',
                         'Число сессий за первый временной интервал',
                         'Число сессий за второй временной интервал',
                         'Количественная разница суммарной глубины просмотра',
                         'Количественная разница числа сессий',
                         'Процентная разница суммарной глубины просмотра',
                         'Процентная разница числа сессий'])
In [23]:
# Добавим два столбца, в которым посчитаем среднее количество просмотренных страниц за сессию
df_fourth_task['Средняя глубина просмотра за первый временной интервал'] = round(df_fourth_task['Суммарная глубина просмотра за первый временной интервал'] / df_fourth_task['Число сессий за первый временной интервал'],2)
df_fourth_task['Средняя глубина просмотра за второй временной интервал'] = round(df_fourth_task['Суммарная глубина просмотра за второй временной интервал'] / df_fourth_task['Число сессий за второй временной интервал'],2)
In [24]:
df_fourth_task.head(3)
Out[24]:
URL Суммарная глубина просмотра за первый временной интервал Суммарная глубина просмотра за второй временной интервал Число сессий за первый временной интервал Число сессий за второй временной интервал Количественная разница суммарной глубины просмотра Количественная разница числа сессий Процентная разница суммарной глубины просмотра Процентная разница числа сессий Средняя глубина просмотра за первый временной интервал Средняя глубина просмотра за второй временной интервал
0 https://metrica.yandex.com/about 27979 23616 19001 15752 -4363 -3249 -15.59 -17.1 1.47 1.5
1 https://metrica.yandex.com/about/info/privacy-... 1609 3073 1376 2757 1464 1381 90.99 100.36 1.17 1.11
2 https://metrica.yandex.com/about/info/integrat... 653 192 387 109 -461 -278 -70.6 -71.83 1.69 1.76

Построим гистаграмму и посмотрим на полученное распределение средней глубины просмотра:

In [25]:
sns.set_style("white")
plt.figure(figsize=(15,8))
ax = sns.kdeplot(data=df_fourth_task, x="Средняя глубина просмотра за первый временной интервал", color='orange')
sns.kdeplot(data=df_fourth_task, x="Средняя глубина просмотра за второй временной интервал",  color='black', ax=ax)
plt.xlabel("Средняя глубина просмотра")
plt.ylabel("Частота")
plt.title("Гистограмма средней глубины просмотра")
plt.legend(['Первый временной интервал','Второй временной интервал'])
plt.show()

Несмотря на то, что визуально оба распределения средней глубины просмотра частично похожи на нормальные, оба таковыми не являются. Докажем это по критерию Шапиро-Уилка:

In [26]:
for i in ['Средняя глубина просмотра за первый временной интервал','Средняя глубина просмотра за второй временной интервал']: 
    stat, p = scipy.stats.shapiro(df_fourth_task.loc[:,i]) 
    print(df_fourth_task.loc[:, i].name)
    display(HTML(f"<div style='text-align:center'>{'Значение p-value = %.3f' % (p)}</div>"))
    alpha = 0.05
    if p > alpha:
        print('Отклоняем гипотезу о нормальности распределения')
    else:
        print('Отклоняем гипотезу о нормальности распределения\n')
Средняя глубина просмотра за первый временной интервал
Значение p-value = 0.001
Отклоняем гипотезу о нормальности распределения

Средняя глубина просмотра за второй временной интервал
Значение p-value = 0.034
Отклоняем гипотезу о нормальности распределения

Из датафрейма df_fourth_task сформируем другой датафрейм df_fourth_task_end, в котором разграничение метрик по временным интервалам будет выполняться не по столбцам, а по строкам. Флаг указывающий на то, какой именно временной интервал записан в любой строке будет храниться в отдельном столбце Временной интервал.

Нужно это для того, чтобы можно было построить несколько графиков на одном, разграничив их через такие параметры, как symbol, color и т.д.

In [27]:
# Дублируем строки и в продублированных выставляем флаг 'True'
df_fourth_task_end = pd.concat([df_fourth_task] * 2).sort_index().reset_index(drop=True)
df_fourth_task_end['Временной интервал'] = df_fourth_task_end.duplicated().astype(str)
# Заменяем 'False' на 'Первый временной интервал', а True' на 'Второй временной интервал'
df_fourth_task_end['Временной интервал'] = df_fourth_task_end.replace('False','Первый временной интервал').replace('True','Второй временной интервал')['Временной интервал']
# Склеиваем два датафрейма, одновременно с этим переименовывая столбцы
df_fourth_task_end = pd.concat([df_fourth_task_end.loc[df_fourth_task_end['Временной интервал'] == 'Первый временной интервал'][['URL','Суммарная глубина просмотра за первый временной интервал', 'Число сессий за первый временной интервал','Временной интервал', 'Средняя глубина просмотра за первый временной интервал']].rename({'Суммарная глубина просмотра за первый временной интервал':'Суммарная глубина просмотра','Число сессий за первый временной интервал':'Число сессий', 'Средняя глубина просмотра за первый временной интервал':'Средняя глубина просмотра'},axis=1), df_fourth_task_end.loc[df_fourth_task_end['Временной интервал'] == 'Второй временной интервал'][['URL','Суммарная глубина просмотра за второй временной интервал', 'Число сессий за второй временной интервал', 'Временной интервал', 'Средняя глубина просмотра за второй временной интервал']].rename({'Суммарная глубина просмотра за второй временной интервал':'Суммарная глубина просмотра','Число сессий за второй временной интервал':'Число сессий', 'Средняя глубина просмотра за второй временной интервал':'Средняя глубина просмотра'},axis=1)]).sort_index() 

Построим два графика глубины просмотра за два временных интервала, чтобы проследить зависимость между количеством визитов и глубиной просмотра по разным URL. Также не забудем перевести масштаб координатных осей в логарифмический, чтобы сохранить равнозначный масштаб для всех значений Добавим в каждый из них по три прямые линии:

  1. Темно-зеленая линия - это граница, разделяющая среднюю глубину просмотра равную единице
  2. Фиолетовая линия - это граница, разделяющая среднюю глубину просмотра равную полутора
  3. Оранжевая линия - это граница, разделяющая среднюю глубину просмотра равную двум
In [28]:
# Строим диаграмму и логарифмируем координатные оси
fig = px.scatter(df_fourth_task_end, y="Суммарная глубина просмотра", color='Временной интервал',hover_data =['Средняя глубина просмотра'],
                 x="Число сессий", hover_name = 'URL',
                log_x = True,log_y=True, 
                labels={"Число сессий": "Суммарное число сессий"},
                title="График глубины просмотра за первый временной интервал", 
                       color_continuous_scale=px.colors.qualitative.Pastel)

# Изменим стиль точек
fig.update_traces(marker=dict(size=8,symbol="square",
                              line=dict(width=0.5,
                                        color='black')),
                  selector=dict(mode='markers'))

# Добавим три прямые линии
fig.add_trace(
    go.Scatter(x=[1, 30000],
               y=[1, 30000],
               mode="lines",
               line=go.scatter.Line(color="darkgreen"),
               showlegend=False),
    row=1, 
    col=1, 
)


fig.add_trace(
    go.Scatter(x=[1, 30000],
               y=[1.5, 45000],
               mode="lines",
               line=go.scatter.Line(color="orchid"),
               showlegend=False),
    row=1, 
    col=1, 
)

fig.add_trace(
    go.Scatter(x=[1, 30000],
               y=[2, 60000],
               mode="lines",
               line=go.scatter.Line(color="coral"),
               showlegend=False),
    row=1, 
    col=1, 
)

# Сменим цвет фона и осей абсцисс, ординат
fig.update_layout(
    plot_bgcolor='aliceblue'
)
fig.update_xaxes(
    mirror=True,
    ticks='outside',
    showline=True,
    linecolor='black',
    gridcolor='lightgrey'
)
fig.update_yaxes(
    mirror=True,
    ticks='outside',
    showline=True,
    linecolor='black',
    gridcolor='lightgrey'
)

fig.update_layout(autosize=False, height=500, width=1000)
fig.show()

Как видно из графика два разных временных интервала мало в целом чем отличаются. Большая часть значений средней глубины просмотра распределилась в районе от 1 до 2. Значения, располагающиеся у начала координат имеют заметно большее среднюю глубину просмотра. Вероятно, это связано с тем, что это низкочастотный трафик. Такой трафик намного более узкоспециализированный и такая аудитория намного больше заинтересована в изучении конечного продукта.

Можно также для более простого понимания построить 3D диаграмму, а в качестве третьей оси выбрать среднюю глубину просмотра, которую мы высчитали ранее:

In [29]:
fig = px.scatter_3d(df_fourth_task_end, hover_name = 'URL',
                    x='Суммарная глубина просмотра',
                    y='Число сессий',
                    z='Средняя глубина просмотра',
                    log_x=True,
                    log_y=True,
                    log_z=True,
                    symbol='Временной интервал',
                    color ='Средняя глубина просмотра',
                    labels={"Число сессий": "Суммарное число сессий"},
                title="График глубины просмотра, 3D", opacity = 0.90, color_continuous_scale=px.colors.qualitative.Pastel)

fig.update_traces(marker=dict(size=10))

fig.update_layout(autosize=False, height=700, width=1000, coloraxis_colorbar_x=-0.15)
fig.show()